//                 _              
//                | |             
//   __ _ _ __ ___| |__ __  _____ 
//  / _` | '__/ __| '_ \\ \/ / __|
// | (_| | | | (__| | | |>  <\__ \
//  \__,_|_|  \___|_| |_/_/\_\___/
//                                
// Copyright 2010 Scott Dixon http://www.archxs.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package com.archxs.commons;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The default implementation of {@link VariantObjectCache}.
 */
class VariantObjectCacheDefault implements VariantObjectCache {
    
    private final ReferenceQueue<Variant> m_referenceQueue;
    
    private final Map<Locale,Map<String,WeakReference<Variant> > > m_cache;
    
    VariantObjectCacheDefault() {
        this(new ReferenceQueue<Variant>());
    }
   
    VariantObjectCacheDefault(final ReferenceQueue<Variant> referenceQueue) {
        m_referenceQueue = referenceQueue;
        m_cache = new ConcurrentHashMap<Locale,Map<String,WeakReference<Variant> > >();
    }
    
    // +-----------------------------------------------------------------------+
    // | VariantObjectCache
    // +-----------------------------------------------------------------------+
    @Override
    public int getCacheSize() {
        int size = 0;
        for(final Map<String,WeakReference<Variant> > localeCache : m_cache.values()) {
            size += localeCache.size();
        }
        return size;
    }
    
    @Override
    public void clearCache() {
        while(null != m_referenceQueue.poll()){}
        m_cache.clear();
    }
    
    @Override
    public Variant readThroughCache(final Class<? extends Variant> type, final String value) {
        return readThroughCache(type, value, Locale.getDefault());
    }
    
    @Override
    public Variant readThroughCache(final Class<? extends Variant> type, final String value, final Locale locale) {
        Variant cachedVariant = getHelper(type, value, locale);
        if (null == cachedVariant) {
            cachedVariant = createNewVariant(type, value, locale);
            final WeakReference<Variant> cachedReference = new WeakReference<Variant>(cachedVariant,m_referenceQueue);
            putHelper(type, value, cachedReference, locale);
        }
        cleanupCache(null);
        return cachedVariant;
    }
    
    @Override
    public Variant readThroughCache(final Class<? extends Variant> type, final String[] value) {
        return readThroughCache(type, value, Locale.getDefault());
    }
    
    @Override
    public Variant readThroughCache(final Class<? extends Variant> type, final String[] value, final Locale locale) {
        Variant cachedVariant = getHelper(type, value, locale);
        if (null == cachedVariant) {
            cachedVariant = createNewVariant(type, value, locale);
            final WeakReference<Variant> cachedReference = new WeakReference<Variant>(cachedVariant,m_referenceQueue);
            putHelper(type, cachedVariant.toString(), cachedReference, locale);
        }
        cleanupCache(null);
        return cachedVariant;
    }
    
    @Override
    public Variant getCachedValue(final String[] value, final Locale locale) {
        return getHelper(value, locale);
    }
    
    @Override
    public Variant getCachedValue(final String value, final Locale locale) {
        return getHelper(value, locale);
    }

    @Override
    public Variant getCachedValue(final String[] value) {
        return getCachedValue(value, Locale.getDefault());
    }
    
    @Override
    public Variant getCachedValue(final String value) {
        return getCachedValue(value, Locale.getDefault());
    }
    
    @Override
    public void setDebug(final boolean debugOnOff) {
        
    }
    
    // +-----------------------------------------------------------------------+
    // | PRIVATE
    // +-----------------------------------------------------------------------+
    @SuppressWarnings("unchecked")
    private <T extends Variant> T createNewVariant(final Class<? extends T> variantType, final String value, final Locale locale) {
        if (variantType == Variant.class) {
            return (T)new Variant(value, locale);
        } else {
            try {
                final Constructor<? extends T> c = variantType.getDeclaredConstructor(String.class, Locale.class);
                c.setAccessible(true);
                return c.newInstance(value, locale);
            } catch (final Exception e) {
                if (e instanceof RuntimeException) {
                    throw (RuntimeException)e;
                }
                throw new IllegalArgumentException(String.format("Cannot instatiate Variant type \"%s\"", variantType), e);
            }
        }
    }
    
    @SuppressWarnings("unchecked")
    private <T extends Variant> T createNewVariant(final Class<? extends T> variantType, final String[] value, final Locale locale) {
        if (variantType == Variant.class) {
            return (T)new Variant(value, locale);
        } else {
            try {
                final Constructor<? extends T> c = variantType.getDeclaredConstructor(String[].class, Locale.class);
                return c.newInstance(value, locale);
            } catch (final Exception e) {
                if (e instanceof RuntimeException) {
                    throw (RuntimeException)e;
                }
                return createNewVariant(variantType, Variant.arrayToString(value, locale), locale);
            }
        }
    }
    
    private void putHelper(final Class<? extends Variant> type, String key, final WeakReference<Variant> value, final Locale locale) {
        if (null == key) {
            key = Variant.NULL_NAME;
        }
        Map<String, WeakReference<Variant> > localeCache = m_cache.get(locale);
        Map<String, WeakReference<Variant> > oldMap = null;
        if (null == localeCache) {
            localeCache = new ConcurrentHashMap<String, WeakReference<Variant> >();
            oldMap = m_cache.put(locale, localeCache);
        }
        localeCache.put(key, value);
        // Ensure stability before continuing
        while(!localeCache.equals(oldMap)) {
            if (null != oldMap) {
                localeCache.putAll(oldMap);
            }
            oldMap = m_cache.put(locale, localeCache);
        }
    }
    
    private Variant getHelper(final String[] key, final Locale locale) {
        return getHelper(null, key, locale);
    }
    
    private Variant getHelper(final Class<? extends Variant> type, final String key[], final Locale locale) {
        return getHelper(type, Variant.arrayToString(key, locale), locale);
    }
    
    private Variant getHelper(final String key, final Locale locale) {
        return getHelper(null, key, locale);
    }
    
    private Variant getHelper(final Class<? extends Variant> type, final String key, final Locale locale) {
        final Map<String, WeakReference<Variant> > localeCache = m_cache.get(locale);
        if (null == localeCache) {
            return null;
        } else {
            final WeakReference<Variant> ref = getReferenceHelper(key, locale);
            if (null == ref) {
                return null;
            } else {
                final Variant value = ref.get();
                if (null == type || (null != value && value.getClass() == type)) {
                    return value;
                } else {
                    return null;
                }
            }
        }
    }
    
    WeakReference<Variant> getReferenceHelper(final String key, final Locale locale) {
        final Map<String, WeakReference<Variant> > localeCache = m_cache.get(locale);
        if (null == localeCache) {
            return null;
        } else {
            if (null == key){
                return localeCache.get(Variant.NULL_NAME);
            } else {
                return localeCache.get(key);
            }
        }
    }
    
    /**
     * Method to cleanup all expired references in the cache.
     * @return <code>true</code> if references were cleaned up else <code>false</code>.
     */
    boolean cleanupCache() {
        //TODO: see if we can find a heuristic to determine this number 
        //      based on cache size. Map.size() has a pretty bad runtime I think
        //      so I decided to not use it.
        return cleanupCache(new Object[10]);
    }
    
    /**
     * Method to cleanup all expired references in the cache.
     * @param  referenceCount An array that can be used to batch reference removal.
     * @return <code>true</code> if references were cleaned up else <code>false</code>.
     */
    boolean cleanupCache(final Object[] referenceCount) {
        boolean didCleanup = false;
        do {
            if (null == referenceCount || referenceCount.length == 0) {
                final Reference<? extends Variant> ref = m_referenceQueue.poll();
                if (null == ref) {
                    break;
                } else {
                    for(final Map<String, WeakReference<Variant> > localizedCache : m_cache.values()) {
                        if (localizedCache.values().remove(ref)) {
                            didCleanup = true;
                            //TODO: atomic/optomistic way of cleaning up the localizedCache
                            //      when it is empty.
                        }
                    }
                }
            } else {
                int i = 0;
                for(; i < referenceCount.length; ++i) {
                    referenceCount[i] = m_referenceQueue.poll();
                    if (null == referenceCount[i]) {
                        break;
                    }
                }
                if (i > 0) {
                    for(final Map<String, WeakReference<Variant> > localizedCache : m_cache.values()) {
                        if (localizedCache.values().removeAll(Arrays.asList(referenceCount))) {
                            //TODO: atomic/optomistic way of cleaning up the localizedCache
                            //      when it is empty. 
                            didCleanup = true;
                        }
                    }
                    if (i < referenceCount.length) {
                        break;
                    }
                    Arrays.fill(referenceCount, null);
                } else {
                    break;
                }
            }
        } while(true);
        return didCleanup;
    }
}
